package peer.SharingManager;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.DigestInputStream;
import java.nio.ByteBuffer;
import java.nio.channels.*;

import peer.BitField;

import common.Util;

/**
 * Represents a reference to a shared file. A FileReference maintains its own state, and
 * is used to read and write chunks of the file it references.
 * @author Andrew Nicholson
 *
 */
public class FileReference {
	
	private File _Handler;
	
	private final String _FileName;
	
	private final long _FileSize;
	
	private final int _NumChunks;
	
	private final int _ChunkSize;
	
	private final String _HashCode;
	
	private BitField _BitField;
	
	private FileInputStream _Reader;
	
	private FileOutputStream _Writer;
	
	private FileChannel _ReadChannel;
	
	private FileChannel _WriteChannel;
	
	public BitField getBitField()
	{
		return _BitField;
	}
	
	public String getFileName()
	{
		return _FileName;
	}
	
	public long getFileSize()
	{
		return _FileSize;
	}
	
	public int getNumberOfChunks()
	{
		return _NumChunks;
	}
	
	public String getHashCode()
	{
		return _HashCode;
	}
	
	/**
	 * Read a chunk of data from the file in binary format.
	 * @param chunkNumber
	 * @return
	 * @throws Exception
	 */
	public byte[] readChunk(int chunkNumber) throws Exception
	{
		int result;
		byte[] data;
		ByteBuffer inBuffer;
		
		if(_Reader == null)
			_Reader = new FileInputStream(_Handler);
		
		if(_ReadChannel == null)
			_ReadChannel = _Reader.getChannel();  
		
		if(chunkNumber > 0)
			_ReadChannel = _ReadChannel.position(chunkNumber * _ChunkSize);
		
		data = new byte[_ChunkSize];
		
		inBuffer = ByteBuffer.wrap(data);
		
		result = _ReadChannel.read(inBuffer);
		
		if(result == _ChunkSize)
			return inBuffer.array();
		else
			throw new Exception("Could not read chunk " + Integer.toString(chunkNumber) + " from file " + _FileName);
	}
	
	public byte[] readBlock(int blockNumber)
	{
		try
		{
			if(_Reader == null)
				_Reader = new FileInputStream(_Handler);
			if(_ReadChannel == null)
				_ReadChannel = _Reader.getChannel();
			
			byte[] data = new byte[(int) Math.min(16 * 1024, _ReadChannel.size() - blockNumber * 16 * 1024)];
			ByteBuffer inBuffer = ByteBuffer.wrap(data);
						
			_ReadChannel.read(inBuffer, blockNumber * 16 * 1024);
			
			return inBuffer.array();
		} catch (FileNotFoundException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		} catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
			return null;
		}
	}
	
	public void writeChunk(int chunkNumber, byte[] data) throws IOException
	{
		boolean isLastChunk = (chunkNumber == _NumChunks - 1);
		long lastChunkSize = _FileSize - (_ChunkSize * (_NumChunks - 1));
		ByteBuffer outBuffer = ByteBuffer.wrap(data);
		
		if(_Writer == null)
			_Writer = new FileOutputStream(_Handler, false);
		
		if(_WriteChannel == null)
			_WriteChannel = _Writer.getChannel();
		
		if((chunkNumber < 0) || (chunkNumber >= _NumChunks))
			throw new IllegalArgumentException("Illegal chunk number!");
		
		int result = _WriteChannel.write(outBuffer, chunkNumber * _ChunkSize);
		
		if((result == -1) || (isLastChunk && (result != lastChunkSize)) || (result != _ChunkSize))
			throw new IOException("Error writing data to file!");
				
	}
	
	public FileReference(String toReference, long size, String hashCode, BitField bitField ) throws Exception
	{
		File tempHandler = new File(toReference);
		
		if(tempHandler.exists() || tempHandler.createNewFile())
		{
			_Handler = tempHandler;
			//_FileName = tempHandler.getCanonicalPath();
			_FileName = tempHandler.getName();
			_FileSize = size;
			_HashCode = hashCode;
			_BitField = bitField;
			_NumChunks = calculateNumChunks();
			
			_ChunkSize = ((_FileSize % _NumChunks) == 0) ? (int)(_FileSize / _NumChunks) : (int)(_FileSize / _NumChunks) + 1;
		}
		else
		{
			throw new Exception("Could not create file" + toReference);
		}
	}
	
	public FileReference(File toReference, long size) throws Exception
	{
		File tempHandler =toReference;
		
		if(tempHandler.exists() || tempHandler.createNewFile())
		{
			_Handler = tempHandler;
			//_FileName = tempHandler.getCanonicalPath();
			_FileName = tempHandler.getName();
			_FileSize = size;
			_HashCode = calculateHashCode();
			_NumChunks = calculateNumChunks();
			
			_ChunkSize = ((_FileSize % _NumChunks) == 0) ? (int)(_FileSize / _NumChunks) : (int)(_FileSize / _NumChunks) + 1;
			
			_BitField = new BitField(_NumChunks);
			
			for(int i = 0; i < _NumChunks; i++)
				_BitField.setHaveChunk(i);
			
		}
		else
		{
			throw new Exception("Could not create file" + toReference);
		}
	}


	private String calculateHashCode() throws FileNotFoundException {
		DigestInputStream _Digest = new DigestInputStream(new FileInputStream(_Handler), null);
		return Util.getHashCode(_Digest);
	}

	private int calculateNumChunks() {
		double temp = Math.log(_FileSize/100) / Math.log(2);
		int temp2 = (int) temp;
		return (temp2 > 0) ? temp2 : 1;
	}
	
	

}
